// Copyright 2003, FreeHEP.
package org.freehep.util.io;

import java.io.IOException;
import java.io.Writer;
import java.util.EventListener;
import java.util.EventObject;
import java.util.TooManyListenersException;

/**
 * Counts line numbers, based on the first cr-lf, cr or lf it finds. Informs a
 * listener when the linenumber exceeds a threshold.
 * 
 * Listeners can only be informed from the second line only.
 * 
 * @author Mark Donszelmann
 * @version $Id: LineNumberWriter.java,v 1.3 2008/05/04 12:21:42 murkle Exp $
 */
public class LineNumberWriter extends Writer {

    private final static int UNKNOWN = 0;

    private final static int CR = 1;

    private final static int CRLF = 2;

    private final static int LF = 3;

    private final static int LFCR = 4;

    private int lineSeparator = UNKNOWN;

    private Writer out;

    private int lineNo = 0;

    private LineNumberListener listener;

    private int lineNoLimit;

    private int previous = -1;

    /**
     * Creates a Line Number Writer
     * 
     * @param out writer to write to
     */
    public LineNumberWriter(Writer out) {
        this.out = out;
    }

    public void write(char cbuf[]) throws IOException {
        write(cbuf, 0, cbuf.length);
    }

    public void write(char cbuf[], int off, int len) throws IOException {
        for (int i = 0; i < len; i++) {
            write(cbuf[off + i]);
        }
    }

    public void write(String str) throws IOException {
        write(str, 0, str.length());
    }

    public void write(String str, int off, int len) throws IOException {
        for (int i = 0; i < len; i++) {
            write(str.charAt(off + i));
        }
    }

    public void write(int c) throws IOException {
        boolean newLine = false;
        synchronized (lock) {
            out.write(c);

            switch (lineSeparator) {
            default:
            case UNKNOWN:
                switch (previous) {
                case '\r':
                    lineNo++;
                    lineSeparator = (c == '\n') ? CRLF : CR;
                    if (c == '\r')
                        lineNo++;
                    break;
                case '\n':
                    lineNo++;
                    lineSeparator = (c == '\r') ? LFCR : LF;
                    if (c == '\n')
                        lineNo++;
                    break;
                default:
                    break;
                }
                break;
            case CR:
                if (c == '\r') {
                    lineNo++;
                    newLine = true;
                }
                break;
            case CRLF:
                if ((previous == '\r') && (c == '\n')) {
                    lineNo++;
                    newLine = true;
                }
                break;
            case LF:
                if (c == '\n') {
                    lineNo++;
                    newLine = true;
                }
                break;
            case LFCR:
                if ((previous == '\n') && (c == '\r')) {
                    lineNo++;
                    newLine = true;
                }
                break;
            }
            previous = c;
        }

        if ((listener != null) && newLine && (lineNo >= lineNoLimit)) {
            listener.lineNumberReached(new LineNumberEvent(this, lineNo));
        }
    }

    public void close() throws IOException {
        out.close();
    }

    public void flush() throws IOException {
        out.flush();
    }

    /**
     * Returns the line number that is currently being written.
     * 
     * @return current line number
     */
    public int getLineNumber() {
        return lineNo;
    }

    /**
     * Set the current line number
     * 
     * @param lineNo new line number
     */
    public void setLineNumber(int lineNo) {
        this.lineNo = lineNo;
    }

    /**
     * Add a LineNumberListener
     * 
     * @param listener new listener
     * @param lineNoLimit line number for which to generate a LineNumberEvent
     * @throws TooManyListenersException if there is more than one listener
     */
    public void addLineNumberListener(LineNumberListener listener,
            int lineNoLimit) throws TooManyListenersException {
        if (this.listener != null)
            throw new TooManyListenersException();
        if (lineNoLimit < 2)
            throw new IllegalArgumentException(
                    "LineNoLimit cannot be less than 2");

        this.listener = listener;
        this.lineNoLimit = lineNoLimit;
    }

    /**
     * LineNumberListener interface can inform a listener about changes in the
     * line number, or when a linenumber limit has been reached.
     * 
     * @author duns
     * @version $Id: LineNumberWriter.java,v 1.3 2008/05/04 12:21:42 murkle Exp $
     */
    public static interface LineNumberListener extends EventListener {

        /**
         * Called when the line number limit has been reached.
         * 
         * @param event line number event
         */
        public void lineNumberReached(LineNumberEvent event);
    }

    /**
     * Event to be used by the LineNumberListener interface.
     * 
     * @author duns
     * @version $Id: LineNumberWriter.java,v 1.3 2008/05/04 12:21:42 murkle Exp $
     */
    public static class LineNumberEvent extends EventObject {
        /**
         * 
         */
        private static final long serialVersionUID = 2821724279014031198L;

        private int lineNo;

        /**
         * Create a LineNumberEvent
         * 
         * @param source event source
         * @param lineNo current line number
         */
        public LineNumberEvent(Object source, int lineNo) {
            super(source);
            this.lineNo = lineNo;
        }

        /**
         * @return current line number
         */
        public int getLineNumber() {
            return lineNo;
        }

        public String toString() {
            return "LineNumberEvent: line=" + lineNo + "; "
                    + getSource().toString();
        }
    }
}
